Skip to content

feat(auth): add OAuth 2.1 for remote MCP deployments#233

Draft
5queezer wants to merge 662 commits intostickerdaniel:mainfrom
5queezer:feature/231-oauth-auth
Draft

feat(auth): add OAuth 2.1 for remote MCP deployments#233
5queezer wants to merge 662 commits intostickerdaniel:mainfrom
5queezer:feature/231-oauth-auth

Conversation

@5queezer
Copy link
Copy Markdown

@5queezer 5queezer commented Mar 19, 2026

Summary

  • Adds opt-in OAuth 2.1 authentication behind --auth oauth for remote server deployments (e.g. Cloud Run, Fly.io)
  • Subclasses FastMCP's InMemoryOAuthProvider with a password-based login page in the /authorize flow
  • Enables claude.ai custom connector integration via Dynamic Client Registration (DCR)

Changes

File Change
linkedin_mcp_server/auth.py NewPasswordOAuthProvider with login page, brute-force lockout (5 attempts)
linkedin_mcp_server/config/schema.py OAuthConfig dataclass + validation (skipped for --login/--status/--logout)
linkedin_mcp_server/config/loaders.py AUTH, OAUTH_BASE_URL, OAUTH_PASSWORD env vars + CLI args
linkedin_mcp_server/server.py Pass auth=PasswordOAuthProvider(...) to FastMCP() when enabled
linkedin_mcp_server/cli_main.py Wire oauth_config through to create_mcp_server()
tests/test_auth.py New — 12 tests: unit + integration (401, .well-known, lockout)
tests/test_config.py 12 new tests for OAuthConfig + env vars + command-only mode bypass
README.md Remote deployment + OAuth section
docs/docker-hub.md OAuth env vars in table
manifest.json OAuth env vars in user_config

Security

  • Timing-safe password comparison (secrets.compare_digest)
  • PKCE enforced by MCP SDK's TokenHandler
  • XSS prevention via html.escape() on all template inputs
  • Brute-force lockout: 5 failed attempts invalidates the auth request
  • 10-minute TTL on pending authorization requests
  • Single-use authorization codes

Test plan

  • 301 tests pass (uv run pytest --cov -v)
  • Pre-commit hooks pass (ruff, ruff-format, ty)
  • Manual E2E: deploy with --auth oauth, verify 401 on unauthenticated /mcp, verify .well-known/oauth-authorization-server returns metadata
  • Manual E2E: complete OAuth flow through claude.ai custom connector

Prompt

Implement the OAuth 2.1 authentication plan from docs/superpowers/plans/2026-03-19-oauth-auth.md

Closes #231

🤖 Generated with Claude Code (Claude Opus 4.6)

github-actions Bot and others added 30 commits February 1, 2026 22:37
…ependencies

chore(deps): update ci dependencies
…ependencies

chore(deps): update ci dependencies
Point dependency at stickerdaniel/linkedin_scraper fork
(fix/rate-limit-false-positive) to fix detect_rate_limit()
false-firing on React RSC payloads.

Also update docs with detailed release workflow notes and
bump opencode agent models to gpt-5.3-codex.

See also: joeyism/linkedin_scraper#278
…el#139)

Point dependency at stickerdaniel/linkedin_scraper fork
(fix/rate-limit-false-positive) to fix detect_rate_limit()
false-firing on React RSC payloads.

Also update docs with detailed release workflow notes and
bump opencode agent models to gpt-5.3-codex.

See also: joeyism/linkedin_scraper#278
Replace Playwright with Patchright (anti-detection fork) and use
launch_persistent_context(user_data_dir=...) for full Chromium profile
persistence. This fixes cross-platform session issues where sessions
created on macOS failed in Docker (Linux, headless).

BREAKING CHANGE: Old session.json files and LINKEDIN_COOKIE env var
are no longer supported. Users must re-run --get-session to create a
new persistent browser profile at ~/.linkedin-mcp/profile/.
…aniel#143)

feat!: Switch to patchright with persistent browser context

Replace Playwright with Patchright (anti-detection fork) and use
launch_persistent_context(user_data_dir=...) for full Chromium profile
persistence. This fixes cross-platform session issues where sessions
created on macOS failed in Docker (Linux, headless).

BREAKING CHANGE: Old session.json files and LINKEDIN_COOKIE env var
are no longer supported. Users must re-run --get-session to create a
new persistent browser profile at ~/.linkedin-mcp/profile/.

polish the implementation
stickerdaniel and others added 7 commits March 16, 2026 02:21
…im_agents.md_to_behavioral_guidance_clean_readme_docker_section

docs: Trim AGENTS.md to behavioral guidance, clean README Docker section
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [anthropics/claude-code-action](https://redirect.github.com/anthropics/claude-code-action) ([changelog](https://redirect.github.com/anthropics/claude-code-action/compare/26ec041249acb0a944c0a47b6c0c13f05dbc5b44..df37d2f0760a4b5683a6e617c9325bc1a36443f6)) | action | digest | `26ec041` → `df37d2f` |
| [astral-sh/setup-uv](https://redirect.github.com/astral-sh/setup-uv) ([changelog](https://redirect.github.com/astral-sh/setup-uv/compare/5a095e7a2014a4212f075830d4f7277575a9d098..37802adc94f370d6bfd71619e3f0bf239e1f3b78)) | action | digest | `5a095e7` → `37802ad` |
| ghcr.io/astral-sh/uv | final | digest | `10902f5` → `3472e43` |
| [oven-sh/setup-bun](https://redirect.github.com/oven-sh/setup-bun) ([changelog](https://redirect.github.com/oven-sh/setup-bun/compare/ecf28ddc73e819eb6fa29df6b34ef8921c743461..0c5077e51419868618aeaa5fe8019c62421857d6)) | action | digest | `ecf28dd` → `0c5077e` |
| python | stage | digest | `5404df0` → `55e465c` |
| [softprops/action-gh-release](https://redirect.github.com/softprops/action-gh-release) ([changelog](https://redirect.github.com/softprops/action-gh-release/compare/a06a81a03ee405af7f2048a818ed3f03bbf83c7b..153bb8e04406b158c6c84fc1615b65b24149a1fe)) | action | digest | `a06a81a` → `153bb8e` |

---

### Configuration

📅 **Schedule**: Branch creation - "before 6am on Monday" (UTC), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/stickerdaniel/linkedin-mcp-server).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My42Ni40IiwidXBkYXRlZEluVmVyIjoiNDMuNjYuNCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
Add opt-in OAuth 2.1 authentication behind `--auth oauth` for remote
server deployments. Subclasses FastMCP's InMemoryOAuthProvider to add a
password-based login page in the /authorize flow, enabling claude.ai
custom connector integration via Dynamic Client Registration.

- PasswordOAuthProvider with HTML login page and brute-force lockout
- OAuthConfig dataclass with validation (skipped for --login/--status/--logout)
- AUTH, OAUTH_BASE_URL, OAUTH_PASSWORD env vars and CLI args
- Integration tests verifying 401→discovery and .well-known endpoints
- Documentation in README, docker-hub.md, and manifest.json

Closes stickerdaniel#231

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@5queezer
Copy link
Copy Markdown
Author

5queezer commented Mar 19, 2026

🔒 OAuth 2.1 Penetration Test Report

Target: https://[REDACTED].europe-west1.run.app
Date: 2026-03-19
Scope: OAuth 2.1 authorization flow, DCR, token endpoint, login page, security headers

Summary

Severity Count
✅ PASS 13
🔴 FAIL 4
🟡 WARN 5
ℹ️ INFO 11

🔴 Findings (FAIL)

F1: DCR accepts dangerous redirect_uri schemes (HIGH — upstream)

Component: MCP Python SDK RegistrationHandler (not our code)
Impact: A malicious client can register with javascript:, data:, or file: redirect URIs. If a user authorizes such a client, the auth code could be exfiltrated.

POST /register
{"redirect_uris": ["javascript:alert(document.cookie)"], ...}
→ HTTP 200 (accepted)

Mitigation: This is an upstream issue in the MCP SDK's DCR validation. Our PasswordOAuthProvider inherits the behavior. A workaround would be to override register_client() to reject non-HTTPS schemes. Recommend filing upstream.

F2: No rate limiting on DCR endpoint (MEDIUM — upstream)

20/20 client registrations succeeded without throttling. An attacker could exhaust server memory by registering unlimited clients.

Mitigation: Same as F1 — inherited from InMemoryOAuthProvider. Could override register_client() with a counter/cap.

F3: Redirect URI case-insensitive hostname match (LOW — likely RFC-compliant)

https://PENTEST.EXAMPLE.COM/callback was accepted when https://pentest.example.com/callback was registered. Per RFC 3986 §3.2.2, hostnames are case-insensitive, so this is technically correct. OAuth 2.1 recommends exact string match, but URL normalization makes this equivalent.

Risk: Minimal in practice — attacker still needs to control the domain.


🟡 Findings (WARN)

W1: No clickjacking protection on /login page

Missing X-Frame-Options and CSP frame-ancestors headers. The login form could be embedded in an attacker's iframe to trick users.

Recommendation: Add X-Frame-Options: DENY to login page responses.

W2: No Content-Security-Policy on /login page

The login page HTML is static with no external resources, so the risk is low. Adding a restrictive CSP would defend-in-depth.

Recommendation: Add Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'

W3-W4: Missing Strict-Transport-Security and X-Content-Type-Options

These should be set by the reverse proxy (Cloud Run handles HSTS for *.run.app domains). Not a code-level issue.


✅ Findings (PASS)

Test Result
Unauthenticated MCP → 401
WWW-Authenticate header present
PKCE enforced (no challenge → rejected)
PKCE plain method rejected (S256 only)
Implicit flow (response_type=token) rejected
XSS via request_id param — properly escaped
Brute-force lockout after 5 attempts
Locked request_id invalidated (HTTP 400)
Fake auth code rejected at token endpoint
client_credentials grant rejected
password (ROPC) grant rejected
OIDC endpoint not exposed
OAuth metadata publicly accessible

Recommendations

  1. Should fix (in this PR): W1+W2 — add security headers to login page HTML responses
  2. Should file upstream: F1+F2 — DCR redirect_uri scheme validation + rate limiting in MCP Python SDK
  3. No action needed: W3-W4 (infrastructure-level), F3 (RFC-compliant)

Tested with automated OAuth2 penetration testing against the live Cloud Run deployment. Authorization: project owner. Tool: custom Python test harness following OWASP OAuth Testing Guide.

Adds X-Frame-Options: DENY, Content-Security-Policy with frame-ancestors
'none', and X-Content-Type-Options: nosniff to all login page responses.
Prevents clickjacking attacks on the password form.

Found by OAuth 2.1 penetration test (W1, W2 in PR stickerdaniel#233 report).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@5queezer 5queezer marked this pull request as ready for review March 19, 2026 20:59
5queezer and others added 2 commits March 19, 2026 22:01
Claude.ai sends requests to the root path by default, which returns 404.
Make it unmistakable that the full MCP endpoint URL is required.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 19, 2026

Greptile Summary

This PR adds opt-in OAuth 2.1 authentication (--auth oauth) for remote MCP deployments by subclassing FastMCP's InMemoryOAuthProvider with a password-based login page, brute-force protection, and Dynamic Client Registration — enabling direct integration with claude.ai custom connectors. The implementation is well-structured and all previously raised security concerns (TTL enforcement, HTTPS validation, path-component rejection, assertValueError guard, multi-instance documentation, process-list password warning) have been addressed in follow-up commits.

Key observations:

  • _render_login skips global lockout check — the GET handler renders the form even when _global_lockout_until is active; the user fills in the form and submits, only to receive a 429 on the POST. The same early-return present in _process_login should also be added to _render_login.
  • No security-event logging — lockout triggers and per-request exhaustion are completely silent in the logs, making it impossible for operators to detect brute-force attacks in their Cloud Run / Fly.io log streams.
  • Redundant per-request lockout check_process_login deletes the pending entry at line 157 but re-checks the same condition at line 174 via the still-live local reference; the logic is correct but confusing; consolidating into a single path would improve readability.
  • Test coverage is thorough (unit + integration, all lockout paths, TTL on GET and POST, security headers, .well-known metadata).

Confidence Score: 4/5

  • Safe to merge — no critical security or correctness bugs; the three flagged items are UX/observability/code-clarity improvements.
  • All previously raised security concerns have been resolved. The remaining issues are: (1) a UX inconsistency where the login form renders during an active global lockout, (2) absence of security-event log lines making attack detection impossible in production, and (3) a slightly confusing double-check of the per-request lockout state. None of these affect the correctness of the authentication mechanism or introduce exploitable vulnerabilities.
  • linkedin_mcp_server/auth.py — review the three style comments around _render_login global lockout, security logging, and the redundant exhaustion check in _process_login.

Important Files Changed

Filename Overview
linkedin_mcp_server/auth.py New PasswordOAuthProvider subclassing InMemoryOAuthProvider; implements login page with TTL enforcement, per-request lockout (5 attempts), global rate-limit (20/5 min → 60 s lockout), XSS-safe HTML generation, and timing-safe password comparison. Minor: _render_login doesn't check global lockout state, no security event logging, and redundant double-check of per-request exhaustion.
linkedin_mcp_server/config/schema.py Adds OAuthConfig dataclass and _validate_oauth on AppConfig; validates HTTPS scheme, rejects path components in base_url, requires password, skips validation for command-only modes (--login/--status/--logout). Clean and complete.
linkedin_mcp_server/config/loaders.py Adds AUTH, OAUTH_BASE_URL, OAUTH_PASSWORD env-var handling and --auth, --oauth-base-url, --oauth-password CLI args. --oauth-password help text warns about process-list visibility. Correct precedence (CLI > env > defaults).
linkedin_mcp_server/server.py Wires PasswordOAuthProvider into FastMCP() when OAuth is enabled; explicit ValueError guards replace previously-flagged assert statements. Clean integration.
tests/test_auth.py 12 tests covering unit (authorize, pending storage, per-request lockout, global rate-limit, TTL on GET and POST, security headers) and integration (401 on unauthenticated /mcp, .well-known metadata, login page accessible without token). Good coverage of all lockout paths.
tests/test_config.py 12 new tests for OAuthConfig defaults, HTTPS requirement, path-component rejection, transport requirement, and command-only mode bypass. Thorough.
README.md New remote-deployment section with Quick Start docker example, Claude.ai connector instructions, env-var/CLI table, single-instance note, and GCP Secret Manager retrieval snippet. Clear and accurate.
linkedin_mcp_server/cli_main.py Passes oauth_config through to create_mcp_server(); no functional issues introduced by the OAuth wiring changes.
linkedin_mcp_server/config/init.py No substantive changes; exports updated to include new OAuthConfig symbol.

Sequence Diagram

sequenceDiagram
    participant Client as claude.ai / MCP Client
    participant Server as LinkedIn MCP Server
    participant Login as /login page

    Client->>Server: POST /mcp (no token)
    Server-->>Client: 401 WWW-Authenticate: Bearer

    Client->>Server: GET /.well-known/oauth-authorization-server
    Server-->>Client: 200 { authorization_endpoint, token_endpoint, registration_endpoint }

    Client->>Server: POST /register (DCR)
    Server-->>Client: 200 { client_id, ... }

    Client->>Server: GET /authorize?client_id=...&code_challenge=...&state=...
    Note over Server: authorize() stores pending_auth_request<br/>(request_id, params, created_at, TTL=10min)
    Server-->>Client: 302 → /login?request_id=<token>

    Client->>Login: GET /login?request_id=<token>
    Note over Login: Check TTL & global lockout
    Login-->>Client: 200 Password form

    Client->>Login: POST /login { request_id, password }
    Note over Login: Check TTL, global lockout (60s window),<br/>compare_digest(), per-request counter (max 5)
    alt Correct password
        Login-->>Client: 302 → redirect_uri?code=...&state=...
        Client->>Server: POST /token { code, code_verifier }
        Server-->>Client: 200 { access_token }
        Client->>Server: POST /mcp (Bearer token)
        Server-->>Client: 200 MCP response
    else Wrong password (< 5 attempts)
        Login-->>Client: 200 Form with error + remaining count
    else Wrong password (5th attempt)
        Login-->>Client: 403 Request invalidated
    else 20 global failures in 5 min
        Login-->>Client: 429 Global lockout (60s)
    end
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/auth.py
Line: 113-126

Comment:
**`_render_login` skips global lockout check**

`_render_login` (GET) validates the TTL but does not check `self._global_lockout_until`. During an active global lockout the login form is still rendered and the user can fill it in — only to receive a 429 when they submit (POST). Adding the same early-return here gives the user an immediately actionable message instead of a two-step confusion:

```suggestion
    async def _render_login(self, request: Request) -> Response:
        request_id = request.query_params.get("request_id", "")
        pending = self._pending_auth_requests.get(request_id) if request_id else None
        if not pending:
            return _html_response("Invalid or expired login request.", status_code=400)

        if time.time() - pending["created_at"] > _PENDING_REQUEST_TTL_SECONDS:
            del self._pending_auth_requests[request_id]
            return _html_response(
                "Login request expired. Please restart the authorization flow.",
                status_code=400,
            )

        if time.time() < self._global_lockout_until:
            return _html_response(
                "Too many failed login attempts. Please try again later.",
                status_code=429,
            )

        return _html_response(self._login_html(request_id))
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: linkedin_mcp_server/auth.py
Line: 153-186

Comment:
**No logging of security events**

The rate-limiting and lockout paths are completely silent. Operators deploying to Cloud Run / Fly.io have no visibility into brute-force attempts without log entries. At minimum, the global lockout trigger and per-request exhaustion should emit a `WARNING`:

```python
# When global lockout is triggered (line ~167):
import logging
logger = logging.getLogger(__name__)
logger.warning(
    "Global OAuth lockout triggered: %d failures in %.0fs window",
    len(self._global_failed_attempts),
    _GLOBAL_RATE_LIMIT_WINDOW_SECONDS,
)

# When per-request attempts exhausted (line ~157):
logger.warning(
    "OAuth request %s exhausted per-request attempts (client_id=%s)",
    request_id,
    pending.get("client_id"),
)
```

Without these, a sustained dictionary attack would be entirely invisible in the server logs.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: linkedin_mcp_server/auth.py
Line: 154-178

Comment:
**Redundant per-request lockout check**

The per-request lockout is checked and the entry deleted at lines 156–157, but the same condition is re-checked at line 174. Because `pending` is still a live local reference after `del self._pending_auth_requests[request_id]`, the logic is correct — but the second check (`pending.get("failed_attempts", 0) >= _MAX_FAILED_ATTEMPTS`) is unreachable when the global-lockout path at lines 166–172 triggers first, and is otherwise always true if we reach that point. Consider consolidating to a single return after the deletion:

```python
if not secrets.compare_digest(password, self._password):
    pending["failed_attempts"] = pending.get("failed_attempts", 0) + 1
    per_req_exhausted = pending["failed_attempts"] >= _MAX_FAILED_ATTEMPTS
    if per_req_exhausted:
        del self._pending_auth_requests[request_id]

    self._global_failed_attempts = [
        t for t in self._global_failed_attempts
        if now - t < _GLOBAL_RATE_LIMIT_WINDOW_SECONDS
    ]
    self._global_failed_attempts.append(now)
    if len(self._global_failed_attempts) >= _GLOBAL_MAX_FAILED_ATTEMPTS:
        self._global_lockout_until = now + _GLOBAL_LOCKOUT_SECONDS
        return _html_response(
            "Too many failed login attempts. Please try again later, "
            "then restart the authorization flow from your client.",
            status_code=429,
        )

    if per_req_exhausted:
        return _html_response(
            "Too many failed attempts. Please restart the authorization flow.",
            status_code=403,
        )
    remaining = _MAX_FAILED_ATTEMPTS - pending["failed_attempts"]
    return _html_response(
        self._login_html(request_id, error=f"Invalid password. {remaining} attempt(s) remaining."),
        status_code=200,
    )
```

This removes the ambiguity around reading `pending` after it has been removed from the dict and makes the control flow unambiguous.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "fix(auth): enforce T..."

Comment thread linkedin_mcp_server/auth.py
Comment thread linkedin_mcp_server/auth.py
Comment thread linkedin_mcp_server/server.py Outdated
Comment thread linkedin_mcp_server/auth.py
…nt, assert removal

- Add global rate limiter (20 failures / 5 min window → 60s lockout) to
  prevent brute-force bypass via fresh request_ids
- Enforce TTL at form submission time, not only during cleanup
- Replace assert with explicit ValueError for oauth_config guards
- Document single-instance requirement for in-memory OAuth state

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment on lines +298 to +304
parser.add_argument(
"--oauth-password",
type=str,
default=None,
metavar="PASSWORD",
help="Password for the OAuth login page",
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 --oauth-password exposes secret in process listing

Passing the OAuth password as a CLI argument makes it visible in /proc/<pid>/cmdline, ps aux output, and shell history (~/.bash_history, ~/.zsh_history). On shared or multi-tenant hosts (including many cloud VMs), any process on the system can read /proc/<pid>/cmdline without elevated privileges.

The environment variable path (OAUTH_PASSWORD) is the safer approach and is already the primary method documented in the README. Consider removing the --oauth-password CLI flag entirely and directing users to the env var instead. If the flag must be kept, at minimum the help text should warn about this risk:

Suggested change
parser.add_argument(
"--oauth-password",
type=str,
default=None,
metavar="PASSWORD",
help="Password for the OAuth login page",
)
parser.add_argument(
"--oauth-password",
type=str,
default=None,
metavar="PASSWORD",
help="Password for the OAuth login page (WARNING: visible in process list; prefer OAUTH_PASSWORD env var)",
)
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/config/loaders.py
Line: 298-304

Comment:
**`--oauth-password` exposes secret in process listing**

Passing the OAuth password as a CLI argument makes it visible in `/proc/<pid>/cmdline`, `ps aux` output, and shell history (`~/.bash_history`, `~/.zsh_history`). On shared or multi-tenant hosts (including many cloud VMs), any process on the system can read `/proc/<pid>/cmdline` without elevated privileges.

The environment variable path (`OAUTH_PASSWORD`) is the safer approach and is already the primary method documented in the README. Consider removing the `--oauth-password` CLI flag entirely and directing users to the env var instead. If the flag must be kept, at minimum the help text should warn about this risk:

```suggestion
    parser.add_argument(
        "--oauth-password",
        type=str,
        default=None,
        metavar="PASSWORD",
        help="Password for the OAuth login page (WARNING: visible in process list; prefer OAUTH_PASSWORD env var)",
    )
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a4b5d2e. Added warning to the help text: (visible in process list; prefer OAUTH_PASSWORD env var). Kept the flag for convenience in dev/testing but the env var is the documented primary path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread linkedin_mcp_server/config/schema.py
Apply greptile suggestion

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Comment thread linkedin_mcp_server/auth.py
- Add restart guidance to 429 global lockout message (prevents dead-end
  when per-request + global lockout collide simultaneously)
- Add restart guidance to "Client not found" error message
- Validate base_url has no path component in _validate_oauth (prevents
  silent 404 when base_url contains e.g. /api)
- Add tests for HTTPS validation, path-component rejection, and
  trailing-slash acceptance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@5queezer
Copy link
Copy Markdown
Author

All three issues from review round 2 addressed in 9ef05c6:

  1. base_url path-component validation — Added urlparse path check in _validate_oauth(). URLs like https://example.com/api are rejected at startup with a clear error.

  2. Misleading 429 on simultaneous lockout collision — 429 message now includes restart guidance: "…try again later, then restart the authorization flow from your client."

  3. "Client not found" recovery guidance — Error message now says: "Client registration not found. Please restart the authorization flow from your client."

All with test coverage. CI passing.

Comment thread linkedin_mcp_server/auth.py
…ering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@5queezer
Copy link
Copy Markdown
Author

Known limitation: OAuth protects the MCP endpoint, but LinkedIn session cookies (used for scraping) are still lost on container restart. Deployments on ephemeral runtimes like Cloud Run need a separate storage backend to persist auth state across cold starts.

Follow-up: feat: GCS-backed auth state persistence for Cloud Run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(auth): OAuth 2.1 support for remote MCP deployments (Cloud Run, etc.)

4 participants